/**
 * ***************************************************************************** Copyright (c)
 * 2012-2015 Red Hat, Inc. All rights reserved. This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * <p>Contributors: Red Hat, Inc. - initial API and implementation
 * *****************************************************************************
 */
package org.eclipse.che.ide.ext.java.jdt.text.edits;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.eclipse.che.ide.api.editor.text.BadLocationException;
import org.eclipse.che.ide.api.editor.text.Region;
import org.eclipse.che.ide.api.editor.text.RegionImpl;
import org.eclipse.che.ide.ext.java.jdt.text.Document;

/**
 * A text edit describes an elementary text manipulation operation. Edits are executed by applying
 * them to a document (e.g. an instance of <code>Document
 * </code>).
 *
 * <p>Text edits form a tree. Clients can navigate the tree upwards, from child to parent, as well
 * as downwards. Newly created edits are un-parented. New edits are added to the tree by calling one
 * of the <code>add</code> methods on a parent edit.
 *
 * <p>An edit tree is well formed in the following sense:
 *
 * <ul>
 *   <li>a parent edit covers all its children
 *   <li>children don't overlap
 *   <li>an edit with length 0 can't have any children
 * </ul>
 *
 * Any manipulation of the tree that violates one of the above requirements results in a <code>
 * MalformedTreeException</code>.
 *
 * <p>Insert edits are represented by an edit of length 0. If more than one insert edit exists at
 * the same offset then the edits are executed in the order in which they have been added to a
 * parent. The following code example:
 *
 * <p>
 *
 * <pre>
 * Document document = new DocumentImpl(&quot;org&quot;);
 * MultiTextEdit edit = new MultiTextEdit();
 * edit.addChild(new InsertEdit(0, &quot;www.&quot;));
 * edit.addChild(new InsertEdit(0, &quot;eclipse.&quot;));
 * edit.apply(document);
 * </pre>
 *
 * <p>therefore results in string: "www.eclipse.org".
 *
 * <p>Text edits can be executed in a mode where the edit's region is updated to reflect the edit's
 * position in the changed document. Region updating is enabled by default or can be requested by
 * passing <code>UPDATE_REGIONS</code> to the {@link #apply(Document, int) apply(Document, int)}
 * method. In the above example the region of the <code>InsertEdit(0, "eclipse.")</code> edit after
 * executing the root edit is <code>[3, 8]</code>. If the region of an edit got deleted during
 * change execution the region is set to <code>[-1, -1]</code> and the method {@link #isDeleted()
 * isDeleted} returns <code>true</code>. This class isn't intended to be subclassed outside of the
 * edit framework. Clients are only allowed to subclass <code>MultiTextEdit</code>.
 *
 * @noextend This class is not intended to be subclassed by clients.
 */
public abstract class TextEdit {

  /**
   * Flags indicating that neither <code>CREATE_UNDO</code> nor <code>UPDATE_REGIONS</code> is set.
   */
  public static final int NONE = 0;

  /**
   * Flags indicating that applying an edit tree to a document is supposed to create a corresponding
   * undo edit. If not specified <code>null</code> is returned from method <code>
   * apply</code>.
   */
  public static final int CREATE_UNDO = 1 << 0;

  /**
   * Flag indicating that the edit's region will be updated to reflect its position in the changed
   * document. If not specified when applying an edit tree to a document the edit's region will be
   * arbitrary. It is even not guaranteed that the tree is still well formed.
   */
  public static final int UPDATE_REGIONS = 1 << 1;

  private static class InsertionComparator implements Comparator<TextEdit> {
    public int compare(TextEdit edit1, TextEdit edit2) throws MalformedTreeException {
      int offset1 = edit1.getOffset();
      int length1 = edit1.getLength();

      int offset2 = edit2.getOffset();
      int length2 = edit2.getLength();

      if (offset1 == offset2 && length1 == 0 && length2 == 0) {
        return 0;
      }
      if (offset1 + length1 <= offset2) {
        return -1;
      }
      if (offset2 + length2 <= offset1) {
        return 1;
      }
      throw new MalformedTreeException(null, edit1, "Overlapping text edits"); // $NON-NLS-1$
    }
  }

  private static final TextEdit[] EMPTY_ARRAY = new TextEdit[0];

  private static final InsertionComparator INSERTION_COMPARATOR = new InsertionComparator();

  private static final int DELETED_VALUE = -1;

  private int fOffset;

  private int fLength;

  private TextEdit fParent;

  private List<TextEdit> fChildren;

  int fDelta;

  /**
   * Create a new text edit. Parent is initialized to <code>
   * null<code> and the edit doesn't have any children.
   *
   * @param offset
   *         the edit's offset
   * @param length
   *         the edit's length
   */
  protected TextEdit(int offset, int length) {
    // Assert.isTrue(offset >= 0 && length >= 0);
    fOffset = offset;
    fLength = length;
    fDelta = 0;
  }

  /**
   * Copy constructor
   *
   * @param source the source to copy form
   */
  protected TextEdit(TextEdit source) {
    fOffset = source.fOffset;
    fLength = source.fLength;
    fDelta = 0;
  }

  // ---- Region management -----------------------------------------------

  /**
   * Returns the range that this edit is manipulating. The returned <code>IRegion</code> contains
   * the edit's offset and length at the point in time when this call is made. Any subsequent
   * changes to the edit's offset and length aren't reflected in the returned region object.
   *
   * <p>Creating a region for a deleted edit will result in an assertion failure.
   *
   * @return the manipulated region
   */
  public final Region getRegion() {
    return new RegionImpl(getOffset(), getLength());
  }

  /**
   * Returns the offset of the edit. An offset is a 0-based character index. Returns <code>-1</code>
   * if the edit is marked as deleted.
   *
   * @return the offset of the edit
   */
  public int getOffset() {
    return fOffset;
  }

  /**
   * Returns the length of the edit. Returns <code>-1</code> if the edit is marked as deleted.
   *
   * @return the length of the edit
   */
  public int getLength() {
    return fLength;
  }

  /**
   * Returns the inclusive end position of this edit. The inclusive end position denotes the last character of the region
   * manipulated by this edit. The returned value is the result of the following calculation:
   * <p/>
   * <pre>
   * getOffset() + getLength() - 1;
   *
   * <pre>
   *
   * @return the inclusive end position
   */
  public final int getInclusiveEnd() {
    return getOffset() + getLength() - 1;
  }

  /**
   * Returns the exclusive end position of this edit. The exclusive end position denotes the next
   * character of the region manipulated by this edit. The returned value is the result of the
   * following calculation:
   *
   * <p>
   *
   * <pre>
   *   getOffset() + getLength();
   * </pre>
   *
   * @return the exclusive end position
   */
  public final int getExclusiveEnd() {
    return getOffset() + getLength();
  }

  /**
   * Returns whether this edit has been deleted or not.
   *
   * @return <code>true</code> if the edit has been deleted; otherwise <code>false</code> is
   *     returned.
   */
  public final boolean isDeleted() {
    return fOffset == DELETED_VALUE && fLength == DELETED_VALUE;
  }

  /**
   * Move all offsets in the tree by the given delta. This node must be a root node. The resulting
   * offsets must be greater or equal to zero.
   *
   * @param delta the delta
   * @since 3.1
   */
  public final void moveTree(int delta) {
    // Assert.isTrue(fParent == null);
    // Assert.isTrue(getOffset() + delta >= 0);
    internalMoveTree(delta);
  }

  /**
   * Returns <code>true</code> if the edit covers the given edit <code>other</code>. It is up to the concrete text edit to decide
   * if a edit of length zero can cover another edit.
   *
   * @param other
   *         the other edit
   * @return <code>true<code> if the edit covers the other edit;
   * otherwise <code>false</code> is returned.
   */
  public boolean covers(TextEdit other) {
    if (getLength() == 0 && !canZeroLengthCover()) return false;

    if (!other.isDefined()) return true;

    int thisOffset = getOffset();
    int otherOffset = other.getOffset();
    return thisOffset <= otherOffset && otherOffset + other.getLength() <= thisOffset + getLength();
  }

  /**
   * Returns <code>true</code> if an edit with length zero can cover another edit. Returns <code>
   * false</code> otherwise.
   *
   * @return whether an edit of length zero can cover another edit
   */
  protected boolean canZeroLengthCover() {
    return false;
  }

  /**
   * Returns whether the region of this edit is defined or not.
   *
   * @return whether the region of this edit is defined or not
   * @since 3.1
   */
  boolean isDefined() {
    return true;
  }

  // ---- parent and children management -----------------------------

  /**
   * Returns the edit's parent. The method returns <code>null</code> if this edit hasn't been add to
   * another edit.
   *
   * @return the edit's parent
   */
  public final TextEdit getParent() {
    return fParent;
  }

  /**
   * Returns the root edit of the edit tree.
   *
   * @return the root edit of the edit tree
   * @since 3.1
   */
  public final TextEdit getRoot() {
    TextEdit result = this;
    while (result.fParent != null) {
      result = result.fParent;
    }
    return result;
  }

  /**
   * Adds the given edit <code>child</code> to this edit.
   *
   * @param child the child edit to add
   * @throws MalformedTreeException is thrown if the child edit can't be added to this edit. This is
   *     the case if the child overlaps with one of its siblings or if the child edit's region isn't
   *     fully covered by this edit.
   */
  public final void addChild(TextEdit child) throws MalformedTreeException {
    internalAdd(child);
  }

  /**
   * Adds all edits in <code>edits</code> to this edit.
   *
   * @param edits the text edits to add
   * @throws MalformedTreeException is thrown if one of the given edits can't be added to this edit.
   * @see #addChild(TextEdit)
   */
  public final void addChildren(TextEdit[] edits) throws MalformedTreeException {
    for (int i = 0; i < edits.length; i++) {
      internalAdd(edits[i]);
    }
  }

  /**
   * Removes the edit specified by the given index from the list of children. Returns the child edit
   * that was removed from the list of children. The parent of the returned edit is set to <code>
   * null</code>.
   *
   * @param index the index of the edit to remove
   * @return the removed edit
   * @throws IndexOutOfBoundsException if the index is out of range
   */
  public final TextEdit removeChild(int index) {
    if (fChildren == null)
      throw new IndexOutOfBoundsException(
          "Index: " + index + " Size: 0"); // $NON-NLS-1$//$NON-NLS-2$
    TextEdit result = (TextEdit) fChildren.remove(index);
    result.internalSetParent(null);
    if (fChildren.isEmpty()) fChildren = null;
    return result;
  }

  /**
   * Removes the first occurrence of the given child from the list of children.
   *
   * @param child the child to be removed
   * @return <code>true</code> if the edit contained the given child; otherwise <code>false</code>
   *     is returned
   */
  public final boolean removeChild(TextEdit child) {
    // Assert.isNotNull(child);
    if (fChildren == null) return false;
    boolean result = fChildren.remove(child);
    if (result) {
      child.internalSetParent(null);
      if (fChildren.isEmpty()) fChildren = null;
    }
    return result;
  }

  /**
   * Removes all child edits from and returns them. The parent of the removed edits is set to <code>
   * null</code>.
   *
   * @return an array of the removed edits
   */
  public final TextEdit[] removeChildren() {
    if (fChildren == null) return EMPTY_ARRAY;
    int size = fChildren.size();
    TextEdit[] result = new TextEdit[size];
    for (int i = 0; i < size; i++) {
      result[i] = (TextEdit) fChildren.get(i);
      result[i].internalSetParent(null);
    }
    fChildren = null;
    return result;
  }

  /**
   * Returns <code>true</code> if this edit has children. Otherwise <code>false</code> is returned.
   *
   * @return <code>true</code> if this edit has children; otherwise <code>false</code> is returned
   */
  public final boolean hasChildren() {
    return fChildren != null && !fChildren.isEmpty();
  }

  /**
   * Returns the edit's children. If the edit doesn't have any children an empty array is returned.
   *
   * @return the edit's children
   */
  public final TextEdit[] getChildren() {
    if (fChildren == null) return EMPTY_ARRAY;
    return (TextEdit[]) fChildren.toArray(new TextEdit[fChildren.size()]);
  }

  /**
   * Returns the size of the managed children.
   *
   * @return the size of the children
   */
  public final int getChildrenSize() {
    if (fChildren == null) return 0;
    return fChildren.size();
  }

  /**
   * Returns the text range spawned by the given array of text edits. The method requires that the
   * given array contains at least one edit. If all edits passed are deleted the method returns
   * <code>
   * null</code>.
   *
   * @param edits an array of edits
   * @return the text range spawned by the given array of edits or <code>null</code> if all edits
   *     are marked as deleted
   */
  public static Region getCoverage(TextEdit[] edits) {
    // Assert.isTrue(edits != null && edits.length > 0);

    int offset = Integer.MAX_VALUE;
    int end = Integer.MIN_VALUE;
    int deleted = 0;
    for (int i = 0; i < edits.length; i++) {
      TextEdit edit = edits[i];
      if (edit.isDeleted()) {
        deleted++;
      } else {
        offset = Math.min(offset, edit.getOffset());
        end = Math.max(end, edit.getExclusiveEnd());
      }
    }
    if (edits.length == deleted) return null;

    return new RegionImpl(offset, end - offset);
  }

  /**
   * Hook called before this edit gets added to the passed parent.
   *
   * @param parent the parent text edit
   */
  void aboutToBeAdded(TextEdit parent) {}

  // ---- Object methods ------------------------------------------------------

  /**
   * The <code>Edit</code> implementation of this <code>Object</code> method uses object identity
   * (==).
   *
   * @param obj the other object
   * @return <code>true</code> iff <code>this == obj</code>; otherwise <code>false</code> is
   *     returned
   * @see Object#equals(java.lang.Object)
   */
  public final boolean equals(Object obj) {
    return this == obj; // equivalent to Object.isEquals
  }

  /**
   * The <code>Edit</code> implementation of this <code>Object</code> method calls uses <code>
   * Object#hashCode()</code> to compute its hash code.
   *
   * @return the object's hash code value
   * @see Object#hashCode()
   */
  public final int hashCode() {
    return super.hashCode();
  }

  /* @see java.lang.Object#toString() */
  public String toString() {
    StringBuffer buffer = new StringBuffer();
    toStringWithChildren(buffer, 0);
    return buffer.toString();
  }

  /**
   * Adds the string representation of this text edit without children information to the given
   * string buffer.
   *
   * @param buffer the string buffer
   * @param indent the indent level in number of spaces
   * @since 3.3
   */
  void internalToString(StringBuffer buffer, int indent) {
    for (int i = indent; i > 0; i--) {
      buffer.append("  "); // $NON-NLS-1$
    }
    buffer.append("{"); // $NON-NLS-1$
    String name = getClass().getName();
    int index = name.lastIndexOf('.');
    if (index != -1) {
      buffer.append(name.substring(index + 1));
    } else {
      buffer.append(name);
    }
    buffer.append("} "); // $NON-NLS-1$
    if (isDeleted()) {
      buffer.append("[deleted]"); // $NON-NLS-1$
    } else {
      buffer.append("["); // $NON-NLS-1$
      buffer.append(getOffset());
      buffer.append(","); // $NON-NLS-1$
      buffer.append(getLength());
      buffer.append("]"); // $NON-NLS-1$
    }
  }

  /**
   * Adds the string representation for this text edit and its children to the given string buffer.
   *
   * @param buffer the string buffer
   * @param indent the indent level in number of spaces
   * @since 3.3
   */
  private void toStringWithChildren(StringBuffer buffer, int indent) {
    internalToString(buffer, indent);
    if (fChildren != null) {
      for (Iterator<TextEdit> iterator = fChildren.iterator(); iterator.hasNext(); ) {
        TextEdit child = iterator.next();
        buffer.append('\n');
        child.toStringWithChildren(buffer, indent + 1);
      }
    }
  }

  // ---- Copying -------------------------------------------------------------

  /**
   * Creates a deep copy of the edit tree rooted at this edit.
   *
   * @return a deep copy of the edit tree
   * @see #doCopy()
   */
  public final TextEdit copy() {
    TextEditCopier copier = new TextEditCopier(this);
    return copier.perform();
  }

  /**
   * Creates and returns a copy of this edit. The copy method should be implemented in a way so that the copy can executed
   * without causing any harm to the original edit. Implementors of this method are responsible for creating deep or shallow
   * copies of referenced object to fulfill this requirement.
   * <p/>
   * Implementers of this method should use the copy constructor <code>
   * Edit#Edit(Edit source) to initialize the edit part of the copy.
   * Implementors aren't responsible to actually copy the children or
   * to set the right parent.
   * <p/>
   * This method <b>should not be called</b> from outside the framework.
   * Please use <code>copy</code> to create a copy of a edit tree.
   *
   * @return a copy of this edit.
   * @see #copy()
   * @see #postProcessCopy(TextEditCopier)
   * @see TextEditCopier
   */
  protected abstract TextEdit doCopy();

  /**
   * This method is called on every edit of the copied tree to do some post-processing like
   * connected an edit to a different edit in the tree.
   *
   * <p>This default implementation does nothing
   *
   * @param copier the copier that manages a map between original and copied edit.
   * @see TextEditCopier
   */
  protected void postProcessCopy(TextEditCopier copier) {}

  // ---- Visitor support -------------------------------------------------

  /**
   * Accepts the given visitor on a visit of the current edit.
   *
   * @param visitor the visitor object
   * @throws IllegalArgumentException if the visitor is null
   */
  public final void accept(TextEditVisitor visitor) {
    // Assert.isNotNull(visitor);
    // begin with the generic pre-visit
    visitor.preVisit(this);
    // dynamic dispatch to internal method for type-specific visit/endVisit
    accept0(visitor);
    // end with the generic post-visit
    visitor.postVisit(this);
  }

  /**
   * Accepts the given visitor on a type-specific visit of the current edit. This method must be
   * implemented in all concrete text edits.
   *
   * <p>General template for implementation on each concrete TextEdit class:
   *
   * <p>
   *
   * <pre>
   * <code>
   * boolean visitChildren= visitor.visit(this);
   * if (visitChildren) {
   *    acceptChildren(visitor);
   * }
   * </code>
   * </pre>
   *
   * <p>Note that the caller (<code>accept</code>) takes care of invoking <code>
   * visitor.preVisit(this)</code> and <code>visitor.postVisit(this)</code>.
   *
   * @param visitor the visitor object
   */
  protected abstract void accept0(TextEditVisitor visitor);

  /**
   * Accepts the given visitor on the edits children.
   *
   * <p>This method must be used by the concrete implementations of <code>accept</code> to traverse
   * list-values properties; it encapsulates the proper handling of on-the-fly changes to the list.
   *
   * @param visitor the visitor object
   */
  protected final void acceptChildren(TextEditVisitor visitor) {
    if (fChildren == null) return;
    Iterator<TextEdit> iterator = fChildren.iterator();
    while (iterator.hasNext()) {
      TextEdit curr = iterator.next();
      curr.accept(visitor);
    }
  }

  // ---- Execution -------------------------------------------------------

  /**
   * Applies the edit tree rooted by this edit to the given document. To check if the edit tree can
   * be applied to the document either catch <code>
   * MalformedTreeException</code> or use <code>TextEditProcessor</code> to execute an edit tree.
   *
   * @param document the document to be manipulated
   * @param style flags controlling the execution of the edit tree. Valid flags are: <code>
   *     CREATE_UNDO</code> and </code>UPDATE_REGIONS</code>.
   * @return a undo edit, if <code>CREATE_UNDO</code> is specified. Otherwise <code>null</code> is
   *     returned.
   * @throws MalformedTreeException is thrown if the tree isn't in a valid state. This exception is
   *     thrown before any edit is executed. So the document is still in its original state.
   * @throws BadLocationException is thrown if one of the edits in the tree can't be executed. The
   *     state of the document is undefined if this exception is thrown.
   * @see TextEditProcessor#performEdits()
   */
  public final UndoEdit apply(Document document, int style)
      throws MalformedTreeException, BadLocationException {
    try {
      TextEditProcessor processor = new TextEditProcessor(document, this, style);
      return processor.performEdits();
    } finally {
      // disconnect from text edit processor
      fParent = null;
    }
  }

  /**
   * Applies the edit tree rooted by this edit to the given document. This method is a convenience
   * method for <code>apply(document, CREATE_UNDO | UPDATE_REGIONS)
   * </code>
   *
   * @param document the document to which to apply this edit
   * @return a undo edit, if <code>CREATE_UNDO</code> is specified. Otherwise <code>null</code> is
   *     returned.
   * @throws MalformedTreeException is thrown if the tree isn't in a valid state. This exception is
   *     thrown before any edit is executed. So the document is still in its original state.
   * @throws BadLocationException is thrown if one of the edits in the tree can't be executed. The
   *     state of the document is undefined if this exception is thrown.
   * @see #apply(Document, int)
   */
  public final UndoEdit apply(Document document)
      throws MalformedTreeException, BadLocationException {
    return apply(document, CREATE_UNDO | UPDATE_REGIONS);
  }

  UndoEdit dispatchPerformEdits(TextEditProcessor processor) throws BadLocationException {
    return processor.executeDo();
  }

  void dispatchCheckIntegrity(TextEditProcessor processor) throws MalformedTreeException {
    processor.checkIntegrityDo();
  }

  // ---- internal state accessors ----------------------------------------------------------

  void internalSetParent(TextEdit parent) {
    // if (parent != null)
    // Assert.isTrue(fParent == null);
    fParent = parent;
  }

  void internalSetOffset(int offset) {
    // Assert.isTrue(offset >= 0);
    fOffset = offset;
  }

  void internalSetLength(int length) {
    // Assert.isTrue(length >= 0);
    fLength = length;
  }

  List<TextEdit> internalGetChildren() {
    return fChildren;
  }

  void internalSetChildren(List<TextEdit> children) {
    fChildren = children;
  }

  void internalAdd(TextEdit child) throws MalformedTreeException {
    child.aboutToBeAdded(this);
    if (child.isDeleted())
      throw new MalformedTreeException(this, child, "Cannot add deleted edit"); // $NON-NLS-1$
    if (!covers(child))
      throw new MalformedTreeException(
          this, child, "Range of child edit lies outside of parent edit"); // $NON-NLS-1$
    if (fChildren == null) {
      fChildren = new ArrayList<TextEdit>(2);
    }
    int index = computeInsertionIndex(child);
    fChildren.add(index, child);
    child.internalSetParent(this);
  }

  private int computeInsertionIndex(TextEdit edit) throws MalformedTreeException {
    int size = fChildren.size();
    if (size == 0) return 0;
    int lastIndex = size - 1;
    TextEdit last = (TextEdit) fChildren.get(lastIndex);
    if (last.getExclusiveEnd() <= edit.getOffset()) return size;
    try {

      int index = Collections.binarySearch(fChildren, edit, INSERTION_COMPARATOR);

      if (index < 0)
        // edit is not in fChildren
        return -index - 1;

      // edit is already in fChildren
      // make sure that multiple insertion points at the same offset are inserted last.
      while (index < lastIndex
          && INSERTION_COMPARATOR.compare(fChildren.get(index), fChildren.get(index + 1)) == 0)
        index++;

      return index + 1;

    } catch (MalformedTreeException e) {
      e.setParent(this);
      throw e;
    }
  }

  // ---- Offset & Length updating -------------------------------------------------

  /**
   * Adjusts the edits offset according to the given delta. This method doesn't update any children.
   *
   * @param delta the delta of the text replace operation
   */
  void adjustOffset(int delta) {
    if (isDeleted()) return;
    fOffset += delta;
    // Assert.isTrue(fOffset >= 0);
  }

  /**
   * Adjusts the edits length according to the given delta. This method doesn't update any children.
   *
   * @param delta the delta of the text replace operation
   */
  void adjustLength(int delta) {
    if (isDeleted()) return;
    fLength += delta;
    // Assert.isTrue(fLength >= 0);
  }

  /** Marks the edit as deleted. This method doesn't update any children. */
  void markAsDeleted() {
    fOffset = DELETED_VALUE;
    fLength = DELETED_VALUE;
  }

  // ---- Edit processing ----------------------------------------------

  /**
   * Traverses the edit tree to perform the consistency check.
   *
   * @param processor the text edit processor
   * @param document the document to be manipulated
   * @param sourceEdits the list of source edits to be performed before the actual tree is applied
   *     to the document
   * @return the number of indirect move or copy target edit children
   */
  int traverseConsistencyCheck(
      TextEditProcessor processor, Document document, List<List<TextEdit>> sourceEdits) {
    int result = 0;
    if (fChildren != null) {
      for (int i = fChildren.size() - 1; i >= 0; i--) {
        TextEdit child = (TextEdit) fChildren.get(i);
        result = Math.max(result, child.traverseConsistencyCheck(processor, document, sourceEdits));
      }
    }
    if (processor.considerEdit(this)) {
      performConsistencyCheck(processor, document);
    }
    return result;
  }

  /**
   * Performs the consistency check.
   *
   * @param processor the text edit processor
   * @param document the document to be manipulated
   */
  void performConsistencyCheck(TextEditProcessor processor, Document document) {}

  /**
   * Traverses the source computation.
   *
   * @param processor the text edit processor
   * @param document the document to be manipulated
   */
  void traverseSourceComputation(TextEditProcessor processor, Document document) {}

  /**
   * Performs the source computation.
   *
   * @param processor the text edit processor
   * @param document the document to be manipulated
   */
  void performSourceComputation(TextEditProcessor processor, Document document) {}

  int traverseDocumentUpdating(TextEditProcessor processor, Document document)
      throws BadLocationException {
    int delta = 0;
    if (fChildren != null) {
      for (int i = fChildren.size() - 1; i >= 0; i--) {
        TextEdit child = (TextEdit) fChildren.get(i);
        delta += child.traverseDocumentUpdating(processor, document);
        childDocumentUpdated();
      }
    }
    if (processor.considerEdit(this)) {
      if (delta != 0) adjustLength(delta);
      int r = performDocumentUpdating(document);
      if (r != 0) adjustLength(r);
      delta += r;
    }
    return delta;
  }

  /**
   * Hook method called when the document updating of a child edit has been completed. When a client
   * calls {@link #apply(Document)} or {@link #apply(Document, int)} this method is called {@link
   * #getChildrenSize()} times.
   *
   * <p>May be overridden by subclasses of {@link MultiTextEdit}.
   *
   * @since 3.1
   */
  protected void childDocumentUpdated() {}

  abstract int performDocumentUpdating(Document document) throws BadLocationException;

  int traverseRegionUpdating(
      TextEditProcessor processor, Document document, int accumulatedDelta, boolean delete) {
    performRegionUpdating(accumulatedDelta, delete);
    if (fChildren != null) {
      boolean childDelete = delete || deleteChildren();
      for (Iterator<TextEdit> iter = fChildren.iterator(); iter.hasNext(); ) {
        TextEdit child = iter.next();
        accumulatedDelta =
            child.traverseRegionUpdating(processor, document, accumulatedDelta, childDelete);
        childRegionUpdated();
      }
    }
    return accumulatedDelta + fDelta;
  }

  /**
   * Hook method called when the region updating of a child edit has been completed. When a client
   * calls {@link #apply(Document)} this method is called {@link #getChildrenSize()} times. When
   * calling {@link #apply(Document, int)} this method is called {@link #getChildrenSize()} times,
   * when the style parameter contains the {@link #UPDATE_REGIONS} flag.
   *
   * <p>May be overridden by subclasses of {@link MultiTextEdit}.
   *
   * @since 3.1
   */
  protected void childRegionUpdated() {}

  void performRegionUpdating(int accumulatedDelta, boolean delete) {
    if (delete) markAsDeleted();
    else adjustOffset(accumulatedDelta);
  }

  abstract boolean deleteChildren();

  void internalMoveTree(int delta) {
    adjustOffset(delta);
    if (fChildren != null) {
      for (Iterator<TextEdit> iter = fChildren.iterator(); iter.hasNext(); ) {
        iter.next().internalMoveTree(delta);
      }
    }
  }

  void deleteTree() {
    markAsDeleted();
    if (fChildren != null) {
      for (Iterator<TextEdit> iter = fChildren.iterator(); iter.hasNext(); ) {
        iter.next().deleteTree();
      }
    }
  }
}
